using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.Practices.Unity;
using MiniStock.BusinessLogic.EventProcessor.EventRepository;
using MiniStock.Entities;
using MiniStock.Entities.Common;
using MiniStock.Entities.Common.Events;
using MiniStock.Infrastructure;

namespace MiniStock.BusinessLogic.EventProcessor.EntityCache
{
    public class EntityCacheBase: IEntityProvider
    {
        [Dependency]
        public IRepository<EventProcessorEventData> Repository { get; set; }
        
        [Dependency]
        public IUnityContainer RootContainer { get; set; }

        private readonly
            Dictionary<string, SortedDictionary<string, object>> internalCache = new Dictionary<string, SortedDictionary<string, object>>();

        public void Initialize()
        {
            List<string> keys;
            // patch for testability
            if(Repository.GetType().Name.Contains("xxxDummyRepository"))
                keys = (from e in Repository.ToList() select e.EntityId).Distinct().ToList();
            else
                keys = (from e in Repository select e.EntityId).Distinct().ToList();

            foreach(var k in keys)
            {
                InitializeKey(k);
            }
        }

        private void InitializeKey(string key)
        {
            var snapshotDate =
                (from e in Repository
                 where e.EntityId == key && e.EventType == "Snapshot"
                 select (DateTime?) e.EventDate).Max();
            var events =
                (from e in Repository
                 where e.EntityId == key && (snapshotDate == null || e.EventDate >= snapshotDate)
                 orderby e.EventDate
                 select e
                ).ToList();

            var eventBase = events[0].ContentXml.ToEventBase();
            EntityBase item = null;
            int startEvent = 0;
            
            if (eventBase is Snapshot)
            {
                item = ((Snapshot)eventBase).Entity;
                startEvent = 1;
            } else if(eventBase is EntityEventBase && ((EntityEventBase)eventBase).IsConstructionMessage)
            {
                Trace.TraceWarning("No snapshot found in entity cache: (eid: {0}, id: {1}, type: {2})", events[0].EntityId, events[0].Id, events[0].EventType);
                item = (EntityBase)RootContainer.Resolve(((EntityEventBase)eventBase).GetEntityType());
            } else
            {
                Trace.TraceError("No construction event found in entity cache: (eid: {0}, id: {1}, type: {2})", events[0].EntityId, events[0].Id, events[0].EventType);
                return;
            }

            for (int i = startEvent; i < events.Count; i++)
                ((EntityEventBase)events[i].ContentXml.ToEventBase()).ApplyTo(item);

            AddEntity(item);
        }

        public bool TryGetItemByHash(string getEntityHash, out EntityBase entity, bool forceLoad = false, TimeSpan? lockTimeout = null)
        {
            bool result = false;

            object item;
            item = AssertLock(getEntityHash, lockTimeout);

            if(item != null && !forceLoad)
            {
                entity = item as EntityBase;
                result = true;
            }
            else
            {
                entity = null;
                // TODO: lookup entity from database, cache item if found and unlocked
                result = false;
            }

            return result;
        }

        public List<T> All<T>()
            where T: EntityBase
        {
            SortedDictionary<string, object> cache;
            internalCache.TryGetValue(typeof (T).Name, out cache);
            var result = new List<T>();
            if(cache != null)
                result = cache.Values.Where(i => i is T).Cast<T>().ToList();
            return result;
        }

        private object AssertLock(string getEntityHash, TimeSpan? lockTimeout)
        {
            object item;
// Wait for lock to be released
            DateTime start = DateTime.Now;
            var parts = getEntityHash.Split('|');
            SortedDictionary<string, object> cache;

            internalCache.TryGetValue(parts[0], out cache);
            if(cache == null)
                return null;

            cache.TryGetValue(parts[1], out item);

            while (item is LockObject && (lockTimeout == null || (DateTime.Now - start) < lockTimeout.Value))
            {
                Thread.Sleep(100);
                cache.TryGetValue(getEntityHash, out item);
            }

            if (item is LockObject)
            {
                throw new ApplicationException("Lock timeout by entity cache");
            }
            return item;
        }

        internal void AddEntity(EntityBase entity)
        {
            if(entity == null)
                throw new ArgumentNullException("entity");

            var hash = entity.GetEntityHash();
            var parts = hash.Split('|');
            if (!internalCache.ContainsKey(parts[0]))
            {
                lock (internalCache)
                {
                    if (!internalCache.ContainsKey(parts[0]))
                        internalCache[parts[0]] = new SortedDictionary<string, object>();
                }
            }
            internalCache[parts[0]][parts[1]] = entity;
        }

        public void LockItem(string key)
        {
            var parts = key.Split('|');
            SortedDictionary<string, object> cache;

            internalCache.TryGetValue(parts[0], out cache);
            if (cache == null) 
                throw new ApplicationException("Key not found");


            if (cache.ContainsKey(key))
            {
                cache[key] = new LockObject(cache[key]);
            }
        }

        public void SubmitPerformanceMetrics(string processorName = "")
        {
            PerformanceMonitoring.SetCounter("Entity cache size", processorName, internalCache.Count);
        }
    }
}